﻿
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;

namespace WSU.ChekaParallel.TestTask
{
    static class TestTaskExceptions
    {
        private static void TestSimpleUsage()
        {
            Func<object, float> mydivide = state =>
            {
                Tuple<float, float> parm = (Tuple<float, float>)state;

                // chekanote: we have to add this checking ourself, because when divided by a zero float
                // CLR will not throw any exception, but view the zero as a correct 
                // argument, and get a result "Infinity"
                if (Math.Abs(parm.Item2) < 1e-6)
                    throw new DivideByZeroException();

                return parm.Item1 / parm.Item2;
            };
            Task<float> normalTask = Task.Factory.StartNew<float>(mydivide, Tuple.Create(1.0f, 3.0f));
            Console.WriteLine("normal task successfully ends with result={0}", normalTask.Result);

            // -------------------- deal with exception
            Task<float> excepTask = Task.Factory.StartNew<float>(mydivide, Tuple.Create(1.0f, 0.0f));
            try
            {
                float impossibleResult = excepTask.Result;
                Debug.Fail("Impossible to come to this line.");
            }
            catch (AggregateException aex)
            {
                Console.WriteLine(aex.InnerException.Message);
            }
        }

        private static void TestIterateAggregateException()
        {
            Task excepTask1 = Task.Factory.StartNew(() =>
                {
                    Thread.Sleep(100);
                    // chekanote: one disadvantage by using AggregateException is that
                    // there is no method to identify which task a specific InnerException comes from
                    // so use "Source" property to add some additional information to linke the original exceptioned task
                    throw new ArgumentOutOfRangeException { Source = "task1" };
                });

            Task excepTask2 = Task.Factory.StartNew(() =>
                {
                    throw new NullReferenceException { Source = "task2" };
                });

            Task normalTask = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("task3 begins running, ... ...");
                    Thread.Sleep(2000);
                    Console.WriteLine("task3 successfully completed");
                });

            try
            {
                // exception wil not escalated immediately, but squirreled away until "WaitAll" is called
                Task.WaitAll(excepTask1, excepTask2, normalTask);
            }
            catch (AggregateException aggExp)
            {
                Console.WriteLine("!!! The first exception is <{0}>", aggExp.InnerException.GetType());

                foreach (Exception exp in aggExp.InnerExceptions)
                    Console.WriteLine("!!! [{0}] with source <{1}>", exp.GetType(), exp.Source);
            }
        }

        private static void RunTaskWithException()
        {
            CancellationTokenSource cancelSource = new CancellationTokenSource();
            CancellationToken cancelToken = cancelSource.Token;

            Task cancelableTask = Task.Factory.StartNew((obj) =>
                {
                    CancellationToken atoken = (CancellationToken)obj;
                    atoken.WaitHandle.WaitOne(-1);// wait forever until canceled
                    throw new OperationCanceledException(atoken);
                }, cancelToken, cancelToken);

            Task exceptionTask = Task.Factory.StartNew(() =>
                {
                    throw new NullReferenceException();
                });

            Thread.Sleep(500);
            cancelSource.Cancel();

            try
            {
                Task.WaitAll(cancelableTask, exceptionTask);
            }
            catch (AggregateException aggExp)
            {
                aggExp.Handle((inexp) =>
                    {
                        if (inexp is OperationCanceledException)
                        {
                            // return true, indicating that exception has been handled, not need to escalated
                            Console.WriteLine("OperationCanceledException is handled inside");
                            return true;
                        }
                        else
                        {
                            // return false, indicating that exception should be escalated and captured by caller
                            // chekanote: exception to be escalted is still AggregateException
                            return false;
                        }
                    });
            }
        }

        private static void TestAggregateExceptionHandler()
        {
            try
            {
                RunTaskWithException();
            }
            catch (AggregateException aggExp)
            {
                Debug.Assert(aggExp.InnerExceptions.Count == 1, "only one left");
                Debug.Assert(aggExp.InnerException is NullReferenceException);

                Console.WriteLine("non-CanceledException is handled outside");
            }
            Console.WriteLine("both exception handled !!");
        }

        private static void TestTaskProperties()
        {
            // ==================== exception task
            Task excepTask = Task.Factory.StartNew(() =>
                {
                    throw new NullReferenceException();
                });

            // ==================== cancelable task
            CancellationTokenSource cancelSource = new CancellationTokenSource();
            CancellationToken cancelToken = cancelSource.Token;

            Task cancelTask = Task.Factory.StartNew(() =>
                {
                    // captured variables, making "cancelToken" a member variable
                    cancelToken.WaitHandle.WaitOne(-1);// wait forever until canceled
                    // chekanote: below codes passing token to the constructor of 'OperationCanceledException'
                    // even without that token, constructor below 'throw new OperationCanceledException();'
                    // will also make the status of the task "Canceled", not "Faulted"
                    throw new OperationCanceledException(cancelToken);
                }, cancelToken);

            cancelSource.Cancel();

            // ==================== wait task completed
            try
            {
                Task.WaitAll(excepTask, cancelTask);
            }
            catch (AggregateException) { }

            // ==================== check properties
            Action<Task> checkAction = task =>
            {
                Debug.Assert(task.IsCompleted);
                Console.WriteLine("IsCanceled? {0}", task.IsCanceled);

                Console.WriteLine("IsFaulted? {0}", task.IsFaulted);
                // task.Exception is still an AggregateException (because multiple exceptions may be embedded)
                Console.WriteLine("Exception? {0}\n", task.Exception == null ? "None" : task.Exception.InnerException.GetType().ToString());
            };
            checkAction(excepTask);
            checkAction(cancelTask);
        }

        private static void TestUnobservedExceptionHandler()
        {
            // chekanote: in the sample, body of the event handler is not invoked
            // just because that the exception thrown during task is executing will only be thrown when task object is finalized
            // (chekanote: an exception is thrown only at three possible steps: Wait, Result, or when the task is GCed.)
            // SO WE CAN JUST VIEW THIS 'UnobservedTaskException' AS THE LAST CHANCE TO CAPTURE THE EXCEPTION
            // and we cannot depend on it for a exception handling strategy
            TaskScheduler.UnobservedTaskException += (object sender, UnobservedTaskExceptionEventArgs evtargs) =>
                {
                    evtargs.SetObserved();// prevent it to be escalated
                    ((AggregateException)evtargs.Exception).Handle((inexp) =>
                        {
                            Console.WriteLine("[{0}] caught.", inexp.GetType().Name);
                            return true;// marked as handled
                        });
                };

            // returned Task is not assigned, so detached, no way to be waited
            Task.Factory.StartNew(() => { throw new NullReferenceException(); });
            Task.Factory.StartNew(() => { throw new ArgumentOutOfRangeException(); });

            Console.WriteLine("Press any key to exit main thread.");
            Console.ReadLine();
        }

        /// <summary>
        /// when dealing with exceptions from child task, first flatten each aggregated exception from each individual child task
        /// into a new aggregated task which removes hierarchy structure
        /// and then use Handle to filter these inner exceptions
        /// </summary>
        private static void TestChildExcepWithFlatAndHandle()
        {
            try
            {
                Task parentTask = Task.Factory.StartNew(() =>
                {
                    int[] numbers = { 0 };

                    TaskFactory childTaskFactory = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.None);

                    childTaskFactory.StartNew(() => 10 / numbers[0]);// divided by zero exception
                    childTaskFactory.StartNew(() => numbers[1]);// index out of range exception
                    childTaskFactory.StartNew(() => { throw new NullReferenceException(); });
                });

                try
                {
                    parentTask.Wait();
                }
                catch (AggregateException aexp)
                {
                    aexp.Flatten().Handle(innerExp =>
                                              {
                                                  if (innerExp is DivideByZeroException)
                                                  {
                                                      Console.WriteLine("Divided by zero exeption is processed internally");
                                                      return true;
                                                  }
                                                  else if (innerExp is IndexOutOfRangeException)
                                                  {
                                                      Console.WriteLine("Index out of range exception is processed internally");
                                                      return true;
                                                  }
                                                  else
                                                      return false;
                                              });
                }
            }
            catch (AggregateException aexp)
            {
                Debug.Assert(aexp.InnerExceptions.Count == 1, "should only one being filtered out");
                Debug.Assert(aexp.InnerException is NullReferenceException);
                Console.WriteLine("only exceptions which are filtered out will be processed externally.");
            }
        }

        /// <summary>
        /// it seems that not always an unhandled exception will crash the whole application
        /// but checking the error will always be a good practice
        /// </summary>
        private static void TestUnhandledException()
        {
            Task errorTask = Task.Factory.StartNew(() => { throw new InvalidOperationException("test"); });

            Helper.Pause();

            // "error" also viewed as "completion"
            Debug.Assert(errorTask.IsCompleted);
            Debug.Assert(errorTask.IsFaulted);

            Console.WriteLine("DONE !!!");
        }

        public static void TestMain()
        {
            // TestIterateAggregateException();
            // TestAggregateExceptionHandler();
            // TestChildExcepWithFlatAndHandle();
            TestTaskProperties();
            // TestUnobservedExceptionHandler();
            // TestSimpleUsage();
            // TestUnhandledException();
        }
    }
}